#' @title This function takes as input a modelbuilder model
#' and creates a diagram using DiagrammeR.
#'
#' @description The model needs to adhere to the structure specified by the modelbuilder package
#' models built using the modelbuilder package automatically have the right structure
#' a user can also build a model list structure themselves following the specifications
#' if the user provides an Rds file name, this file needs to contain an object called 'mbmodel'
#' and contain a valid modelbuilder model structure
#' @param mbmodel model structure, either as list object or Rds file name
#' @return The function returns the diagram stored in a variable
#' @author Andreas Handel and Andrew Tredennick
#' @rawNamespace import(ggplot2, except = last_plot)
#' @export
generate_flowchart <- function(mbmodel) {
# Extract relevant details from the mbmodel and make a matrix
# of variables X flows for iterating and indexing the nodes and
# connections.
nvars = length(mbmodel$var) #number of variables/compartments in model
varnames <- unlist(lapply(mbmodel$var, "[[", 1))
vartext = unlist(sapply(mbmodel$var, '[', 2)) #extract variable text as vector
allflows = sapply(mbmodel$var, '[', 4) #extract flows
#turns flow list into matrix, adding NA, found it online,
# not sure how exactly it works
flowmat = t(sapply(allflows, `length<-`, max(lengths(allflows))))
flowmatred = sub("\\+|-","",flowmat) #strip leading +/- from flows
signmat = gsub("(\\+|-).*","\\1",flowmat) #extract only the + or - signs from flows so we know the direction
# Create a node data frame
ndf <-
data.frame(
id = 1:nvars, #number of nodes
label = varnames #labels of nodes
)
# Create the edge data frame by looping through the variables
# and associated flows.
edf <- list() #an empty list to be coerced to a data frame via rbind
for(i in 1:nrow(flowmatred)) {
varflowsfull = flowmat[i,] #all flows with sign for current variable
varflows = flowmatred[i,] #all flows for current variable
varflowsigns = signmat[i,] #signs of flows for current variable
varflows = varflows[!is.na(varflows)] #remove NA entries
for(j in 1:length(varflows)) {
currentflowfull = varflowsfull[j] #loop through all flows for variable
currentflow = varflows[j] #loop through all flows for variable
currentsign = varflowsigns[j] #loop through all flows for variable
# Find the variables for which the current flow appears, i.e., what
# other rows of the matrix does it show up in.
connectvars <- unname(which(flowmatred == currentflow, arr.ind = TRUE)[,1])
# Extract the variable names
varspars <- unique(get_vars_pars(currentflowfull))
vars <- varspars[which(varspars %in% LETTERS)]
# If the flow does not show up in any other rows BUT starts with
# a plus sign, then the donating node will be the state variable
# in the flow
if(currentsign == "+") {
if(length(connectvars) == 1 & length(vars) == 0) {
connectvars <- i
}
if(length(connectvars) == 1 & length(vars) >= 1){
if(!varnames[i] %in% vars) {
connectvars <- i
}
if(varnames[i] %in% vars) {
connectvars <- c(i, i)
}
}
if(length(connectvars) > 1) {
connectvars <- connectvars
}
}
# if(length(connectvars) == 1 & currentsign == "+") {
# varspars <- get_vars_pars(currentflow)
# var <- varspars[which(varspars %in% LETTERS)]
# cnnew <- which(varnames == var)
# if(length(cnnew) == 1) {
# connectvars <- c(connectvars, cnnew)
# } else {
# connectvars <- c(i, i)
# }
# }
# If current sign is negative, it is an outflow and goes either the
# connectvar that is not equal to the current variable id (indexed by i)
# or it goes to NA (this happens when there is an unspecified death
# compartment, for example).
if(currentsign == "-") {
if(length(connectvars) == 1) {
cn <- NA #placeholder for unspecified compartment (deaths, typically)
} else {
cn <- connectvars[connectvars!=i]
}
tmp <- data.frame(from = i,
to = cn,
label = currentflow)
edf <- rbind(edf, tmp)
}
# If the current sign is positive AND the flow only shows up in
# one row of the flow matrix, then this is an inflow external to the
# system or as a function of the current variable itself. Currently,
# we assume these arise from the variable itself, but we can extend
# this functionality later on.
if(currentsign == "+" & length(connectvars) == 1) {
if(connectvars == i) {
tmp <- data.frame(from = NA,
to = i,
label = currentflow)
edf <- rbind(edf, tmp)
}
}
if(currentsign == "+" & length(connectvars) == 2) {
if(length(unique(connectvars)) == 1) {
tmp <- data.frame(from = i,
to = i,
label = currentflow)
} else {
tmp <- data.frame(from = connectvars[connectvars!=i],
to = i,
label = currentflow)
}
edf <- rbind(edf, tmp)
}
} #end flow loop
} #end variable loop
# Make dummy compartment for all flows in and out of the system.
# Out of the system first
outdummies <- NULL
numnas <- length(edf[is.na(edf$to), "to"])
if(numnas > 0) {
outdummies <- as.numeric(paste0("999", c(1:numnas)))
edf[is.na(edf$to), "to"] <- outdummies
}
# In to the system second
indummies <- NULL
numnas <- length(edf[is.na(edf$from), "from"])
if(numnas > 0) {
indummies <- as.numeric(paste0("-999", c(1:numnas)))
edf[is.na(edf$from), "from"] <- indummies
}
# Add dummy compartments to nodes dataframe
if(is.numeric(outdummies) | is.numeric(indummies)) {
exnodes <- data.frame(id = c(outdummies, indummies),
label = "")
ndf <- rbind(ndf, exnodes)
}
# Keep only distinct rows
edf <- unique(edf)
# Add x and y locations for the nodes
ndf <- ndf[order(ndf$id), ]
ndf$x <- 1:nrow(ndf)*3
ndf$y <- 1
# update inflow node positions from nowhere
inflownodes <- subset(ndf, id < -9990)$id
for(id in inflownodes) {
newxyid <- edf[which(edf$from == id), "to"]
newxy <- ndf[which(ndf$id == newxyid), c("x", "y")]
newxy$y <- newxy$y + 2
ndf[which(ndf$id == id), c("x", "y")] <- newxy
}
# update outflow node positions to nowhere
outflownodes <- subset(ndf, id > 9990)$id
for(id in outflownodes) {
newxyid <- edf[which(edf$to == id), "from"]
newxy <- ndf[which(ndf$id == newxyid), c("x", "y")]
newxy$y <- newxy$y - 2
ndf[which(ndf$id == id), c("x", "y")] <- newxy
}
# update node positions that overlap
xys <- ndf[ , c("x", "y")]
overlapids <- which(duplicated(xys) | duplicated(xys, fromLast = TRUE))
numoverlap <- length(overlapids)
newxs <- seq(0.1, 1.9, by = 0.15)
newxids <- c(6,5,4,3,2,1,0,1,2,3,4,5,6) + 1
xmults <- newxs[which(newxids==numoverlap)]
for(i in 1:numoverlap) {
oldx <- ndf[overlapids[i], "x"]
newx <- oldx * xmults[i]
ndf[overlapids[i], "x"] <- newx
}
# Create segment coordinates
edf <- merge(edf, ndf[ , c("x", "y", "id")], by.x = "from", by.y = "id")
edf <- merge(edf, ndf[ , c("x", "y", "id")], by.x = "to", by.y = "id",
suffixes = c("start", "end"))
edf$xmid <- with(edf, (xend + xstart) / 2)
edf$ymid <- with(edf, (yend + ystart) / 2) + 0.25
edf$diff <- with(edf, abs(to-from))
# split up the edges into constituent parts:
# - curved segments
# - straight (horizontal) segments
# - vertical segments
# - feedback segments (curved back onto same node)
cdf <- subset(edf, (diff > 1 & diff < 9000) & (to != from))
sdf <- subset(edf, diff <= 1 | diff >= 9000)
vdf <- subset(sdf, abs(diff) >= 9990)
sdf <- subset(sdf, abs(diff) < 9990)
fdf <- subset(sdf, to == from)
sdf <- subset(sdf, to != from)
# test to make sure splits are unique and sum up to original data frame
test <- nrow(vdf) + nrow(sdf) + nrow(cdf) + nrow(fdf) == nrow(edf)
if(!test) {
stop(paste0("Edges data frame is not splitting appropriately.\n",
" Contact package maintainer."))
}
# now drop "hidden" nodes without labels
ndf <- subset(ndf, label != "")
# rename data frames for exporting
nodes <- ndf
horizontal_edges <- sdf
vertical_edges <- vdf
curved_edges <- cdf
feedback_edges <- fdf
# make the plot
outplot <- ggplot() +
geom_tile(data = nodes,
aes(x = x, y = y),
color = "black",
fill = "white",
width = 1,
height = 1) +
geom_text(data = nodes,
aes(x = x, y = y, label = label),
size = 8) +
geom_segment(data = horizontal_edges,
aes(x = xstart+0.5, y = ystart, xend = xend-0.5, yend = yend),
arrow = arrow(length = unit(0.25,"cm"), type = "closed"),
arrow.fill = "black",
lineend = "round",
linejoin = "mitre") +
geom_text(data = horizontal_edges,
aes(x = xmid, y = ymid, label = label)) +
geom_segment(data = vertical_edges,
aes(x = xstart, y = ystart-0.5, xend = xend, yend = yend+0.5),
arrow = arrow(length = unit(0.25,"cm"), type = "closed"),
arrow.fill = "black",
lineend = "round",
linejoin = "mitre") +
geom_text(data = vertical_edges,
aes(x = xmid+0.25, y = ymid, label = label)) +
geom_curve(data = curved_edges,
aes(x = xstart, y = ystart+0.5, xend = xend, yend = yend+0.5),
arrow = arrow(length = unit(0.25,"cm"), type = "closed"),
arrow.fill = "black",
lineend = "round") +
geom_text(data = curved_edges,
aes(x = xmid, y = ymid + 2, label = label)) +
geom_curve(data = feedback_edges,
ncp = 100,
curvature = -2,
aes(x = xstart-0.25, y = ystart+0.5, xend = xend+0.25, yend = yend+0.5),
arrow = arrow(length = unit(0.25,"cm"), type = "closed"),
arrow.fill = "black",
lineend = "round") +
geom_text(data = feedback_edges,
aes(x = xmid, y = ymid+0.85, label = label)) +
coord_equal(clip = "off") +
theme_void()
ggcode <- 'ggplot() +
geom_tile(data = nodes,
aes(x = x, y = y),
color = "black",
fill = "white",
width = 1,
height = 1) +
geom_text(data = nodes,
aes(x = x, y = y, label = label),
size = 8) +
geom_segment(data = horizontal_edges,
aes(x = xstart+0.5, y = ystart, xend = xend-0.5, yend = yend),
arrow = arrow(length = unit(0.25,"cm"), type = "closed"),
arrow.fill = "black",
lineend = "round",
linejoin = "mitre") +
geom_text(data = horizontal_edges,
aes(x = xmid, y = ymid, label = label)) +
geom_segment(data = vertical_edges,
aes(x = xstart, y = ystart-0.5, xend = xend, yend = yend+0.5),
arrow = arrow(length = unit(0.25,"cm"), type = "closed"),
arrow.fill = "black",
lineend = "round",
linejoin = "mitre") +
geom_text(data = vertical_edges,
aes(x = xmid+0.25, y = ymid, label = label)) +
geom_curve(data = curved_edges,
aes(x = xstart, y = ystart+0.5, xend = xend, yend = yend+0.5),
arrow = arrow(length = unit(0.25,"cm"), type = "closed"),
arrow.fill = "black",
lineend = "round") +
geom_text(data = curved_edges,
aes(x = xmid, y = ymid + 2, label = label)) +
geom_curve(data = feedback_edges,
ncp = 100,
curvature = -2,
aes(x = xstart-0.25, y = ystart+0.5, xend = xend+0.25, yend = yend+0.5),
arrow = arrow(length = unit(0.25,"cm"), type = "closed"),
arrow.fill = "black",
lineend = "round") +
geom_text(data = feedback_edges,
aes(x = xmid, y = ymid+0.85, label = label)) +
coord_equal(clip = "off") +
theme_void()'
outlist <- list(flowchart = outplot,
dataframes = list(nodes = nodes,
horizontal_edges = horizontal_edges,
vertical_edges = vertical_edges,
curved_edges = curved_edges,
feedback_edges = feedback_edges),
ggplot_code = ggcode)
return(outlist)
}
# library(DiagrammeRsvg)
# library(rsvg)
# render_graph(graph)
# render_graph(plot)
# })
# export_graph(graph = graph, file_type = "png",
# file_name = "../../Desktop/my_crazy_graph.png")
# # Create the DiagrammeR graph object based on the node
# # and edge data frames. We default to graphs being built
# # left-to-right (attr_theme = "lr)
# graph <- create_graph(
# attr_theme = "lr",
# nodes_df = ndf,
# edges_df = edf,
# directed = TRUE
# )
#
# # Update font for nodes
# graph <- set_node_attrs(
# graph = graph,
# node_attr = "fontname",
# values = "Arial" #chose Arial b/c it works across operating systems
# )
#
# # Update edge attributes for color and size of arrows
# graph <- set_edge_attrs(
# graph = graph,
# edge_attr = "color",
# values = "grey35"
# )
#
# graph <- set_edge_attrs(
# graph = graph,
# edge_attr = "arrowsize",
# values = 0.5
# )
#
# return(graph)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.